using System;
using System.Collections.Generic;
using System.Text;
using System.CodeDom.Compiler;
using System.IO;
using System.CodeDom;
using SharpMud.Server.Systems.Scripting.Processing.Metadata;
using SharpMud.Server.Systems.Scripting.Runtime;

namespace SharpMud.Server.Systems.Scripting.Processing.Source
{
    public abstract class ScriptSourceBase : IScriptSourceHandle
    {
        #region Private Fields
        private System.Uri               _SourceCodeLocation;
        private IScriptCompileContext    _CompileContext; 

        private PreparedCodeContainer   _RecentPreparedCode;
        private PreparedCodeContainer   _LastCompiledPreparedCode;

        private IScriptCompileResults   _LastCompileResults;

        private object _CompileSyncLock;
        #endregion

        #region Constructors and Abstract
        private ScriptSourceBase() { }
        protected ScriptSourceBase(System.Uri sourceCodeLocation, IScriptCompileContext compileContext)
        {
            if (sourceCodeLocation == null)
                throw new ArgumentNullException("sourceCodeLocation");
            _SourceCodeLocation = sourceCodeLocation;

            CodeDomProvider compilerForThis = this.GetPreferredCompiler();
            //if (compileContext.PreferredCompiler != null && compilerForThis!=null && compileContext.PreferredCompiler != compilerForThis)
            //{
            //    throw new ArgumentException("This script is not the same language as the one explicitly specified in the ScriptCompileContext.", "compileContext");
            //}
            compileContext.AddScript(this);

            string content = this.GetCodeContents();

            PreparedCodeContainer readiedCode = ScriptSourceBase.PrepareCodeContainer(this._SourceCodeLocation, content, compilerForThis);
            _RecentPreparedCode = readiedCode;

            _CompileSyncLock = new object();
        }

        protected abstract CodeDomProvider GetPreferredCompiler();
        protected abstract string GetCodeContents();
        #endregion

        #region Update
        private void UpdatePreparedCode()
        {
            string newCodeContent = this.GetCodeContents();
            System.CodeDom.Compiler.CodeDomProvider newCodeDomProvider = this.GetPreferredCompiler();
            PreparedCodeContainer newReadiedCode = ScriptSourceBase.PrepareCodeContainer(this._SourceCodeLocation, newCodeContent, newCodeDomProvider);
            if (newReadiedCode == _RecentPreparedCode)
                return;
            if (newReadiedCode.OriginalCodeHash != Guid.Empty &&
                _RecentPreparedCode.OriginalCodeHash != Guid.Empty &&
                newReadiedCode.OriginalCodeHash != _RecentPreparedCode.OriginalCodeHash)
            {
                _RecentPreparedCode = newReadiedCode;

                if (this.Dirtied != null)
                    this.Dirtied(this, EventArgs.Empty);
            }
        }
        private void UpdateCompile()
        {
            if (!this.IsDirty)
                return;
            if (this._CompileSyncLock == null)
                throw new InvalidOperationException("Attempt to execute UpdateCompile() before CompileSyncLock is established or after is was destroyed.");

            lock (this._CompileSyncLock)
            {

                IScriptCompileResults clcr = null;
                clcr = this._CompileContext.GetLastCompileResults(this);
                if (clcr == null && this._LastCompileResults == null)
                {
                    this._CompileContext.Update();
                    clcr = this._CompileContext.GetLastCompileResults(this);

                    this._LastCompiledPreparedCode = this._RecentPreparedCode;
                    this._LastCompileResults = clcr;

                    if (this.Settled != null)
                        this.Settled(this, EventArgs.Empty);

                    return;
                }
                else if (clcr != null && this._LastCompiledPreparedCode != null && clcr != this._LastCompiledPreparedCode)
                {
                    this._LastCompiledPreparedCode = this._RecentPreparedCode;
                    this._LastCompileResults = clcr;

                    if (this.Settled != null)
                        this.Settled(this, EventArgs.Empty);

                    return;
                }
            }
        }

        public bool IsDirty
        {
            get
            {
                UpdatePreparedCode();
                if (_LastCompiledPreparedCode == null)
                    return true;
                else if (_LastCompiledPreparedCode == _RecentPreparedCode)
                    return false;
                else
                    return true;
            }
        }

        public event System.EventHandler Dirtied;
        #endregion

        #region IScriptSourceHandle Members

        public System.CodeDom.CodeCompileUnit GetCompileUnit()
        {
            UpdatePreparedCode();

            CodeCompileUnit ccu;
            using (TextReader txtrdr = new StringReader(_RecentPreparedCode.PreparedCode))
            {
                ccu = _RecentPreparedCode.Compiler.Parse(txtrdr);
            }
            return ccu;
        }

        public Uri ContentLocation
        {
            get { return this._SourceCodeLocation; }
        }

        public System.Guid ContentHashGuid
        {
            get 
            {
                UpdatePreparedCode();
                return this._RecentPreparedCode.OriginalCodeHash; 
            }
        }

        public ICompiledScriptHandle GetCompiledHandle()
        {
            UpdatePreparedCode();
            if (IsDirty)
            { this.UpdateCompile(); }
            if (!LastCompileResults.WasSuccessful)
                throw new NotCompileableException(this, LastCompileResults);
            else
                return LastCompileResults.CompiledScriptHandle;
        }

        public IScriptCompileResults LastCompileResults
        {
            get
            {
                if (IsDirty)
                { this.UpdateCompile(); }
                return _LastCompileResults;
            }
        }

        #endregion

        private static HashWrapper HashText(string text)
        {
            if (text == null)
                throw new ArgumentNullException();

            return HashWrapper.Hash(typeof(System.Security.Cryptography.SHA256Managed), System.Text.Encoding.Default.GetBytes(text));
        }

        /// <summary>
        /// Prepares a script for compile by parsing it, then tagging it's Public (non-Abstract) 
        /// class definitions with instances of ScriptSourceAttribute which will link the scripts
        /// back to their original source-code files.
        /// </summary>
        private static PreparedCodeContainer PrepareCodeContainer(System.Uri codeLocation, string codeContent, CodeDomProvider codeCompiler)
        {
            //Parse the code using CodeDom
            System.CodeDom.CodeCompileUnit ccu;
            using(TextReader codeContentReader = new StringReader(codeContent))
            {
                ccu = codeCompiler.Parse(codeContentReader);
            }
            HashWrapper codeContentHash = ScriptSourceBase.HashText(codeContent);
            System.Guid codeContentHashGuid = codeContentHash.ToGUID();

            //TODO: insert line pragmas to indicate orignal source file


            //Recursively traverse the code...
            foreach (System.CodeDom.CodeNamespace cnamespce in ccu.Namespaces)
            {
                foreach (System.CodeDom.CodeTypeDeclaration ctypedecl in cnamespce.Types)
                {
                    //...and apply ScriptSourceAttribute to only the Public, non-Abstract
                    //class definitions
                    ScriptSourceBase.TagTypeDeclaration(ctypedecl,codeLocation,codeContentHashGuid);
                }
            }

            //TODO: Randomize class/namespace names to prevent script naming conflicts

            //Dump the CodeDom data back out as raw text
            StringBuilder sb = new StringBuilder();
            using (TextWriter sbw = new StringWriter(sb))
            {
                CodeGeneratorOptions cgo = new CodeGeneratorOptions();
                cgo.VerbatimOrder = true;
                codeCompiler.GenerateCodeFromCompileUnit(ccu, sbw, cgo);
            }

            //Return the results
            return new PreparedCodeContainer(codeContentHashGuid, sb.ToString(), codeCompiler);
        }
        private static void TagTypeDeclaration(System.CodeDom.CodeTypeDeclaration ctypedecl, System.Uri codeLocation, System.Guid codeHashGuid)
        {
            if(ctypedecl.IsClass && (((int)(ctypedecl.Attributes & System.CodeDom.MemberAttributes.Public))!=0) && !(((int)(ctypedecl.Attributes & System.CodeDom.MemberAttributes.Abstract))!=0))
            {
                CodeAttributeDeclaration cattdecl;
                cattdecl = new CodeAttributeDeclaration(new CodeTypeReference(typeof(ScriptSourceAttribute)),
                    new CodeAttributeArgument(new CodeObjectCreateExpression(new CodeTypeReference(typeof(System.Uri)), new CodePrimitiveExpression(codeLocation.ToString()))),
                    new CodeAttributeArgument(new CodeObjectCreateExpression(new CodeTypeReference(typeof(System.Guid)), new CodePrimitiveExpression(codeHashGuid.ToString())))
                        );
                ctypedecl.CustomAttributes.Add(cattdecl);
            }
        }

        #region IScriptSourceHandle Members

        public Guid ContentHash
        {
            get { return _RecentPreparedCode.OriginalCodeHash; }
        }
        
        public Type PreferredLanguage
        {
            get { 
                this.UpdatePreparedCode();
                return this._RecentPreparedCode.Compiler.GetType();
            }
        }

        public event EventHandler Settled;

        public void Pulse()
        {
            if (this.IsDirty)
                this.UpdateCompile();
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            this._CompileSyncLock = null;
            this._CompileContext = null;
            this._RecentPreparedCode = null;
            this._LastCompiledPreparedCode = null;
            this._LastCompileResults = null;
            this._SourceCodeLocation = null;

            _Dispose();
        }
        protected abstract void _Dispose();

        #endregion

        #region ICompilerErrorTransformer Members

        public bool IsSource(CompilerError compilerError, IScriptSourceHandle scriptSource)
        {
            System.Uri compilerErrorFileUri = new Uri(compilerError.FileName);
            if (compilerErrorFileUri.Equals(scriptSource.ContentLocation))
            {
                return true;
            }
            else
            {
                return false;
            }            
        }

        public IScriptCompileMessage[] ProcessCompilerErrors(CompilerError[] compilerErrors, IScriptSourceHandle scriptSource)
        {
            List<IScriptCompileMessage> scms = new List<IScriptCompileMessage>();
            foreach (CompilerError ce in compilerErrors)
            {
                IScriptCompileMessage scm = ScriptCompileMessage.FromCompilerError(ce);
                scms.Add(scm);
            }
            return scms.ToArray();
        }

        #endregion
    }
}
